OAuth2接入第三方认证平台实战(登录,注销,当前用户,自定义异常,白名单)

您所在的位置:网站首页 前端 oauth2 OAuth2接入第三方认证平台实战(登录,注销,当前用户,自定义异常,白名单)

OAuth2接入第三方认证平台实战(登录,注销,当前用户,自定义异常,白名单)

#OAuth2接入第三方认证平台实战(登录,注销,当前用户,自定义异常,白名单)| 来源: 网络整理| 查看: 265

1 演示

先来演示一波,再进行讲解

(1)不携带token访问资源

不携带token无法访问资源 在这里插入图片描述 (2)登录

登录之后获取token 在这里插入图片描述 (3)携带token访问资源

通过携带登录获得的token,可以访问到资源。 在这里插入图片描述 (4)注销

将当前的token注销掉。 在这里插入图片描述 (5)注销后访问资源

注销之后,携带当前的token无法访问资源。 在这里插入图片描述 (6)登录后再次访问资源

登录获得新的token,通过新的token可以访问资源。 在这里插入图片描述 (7)获取当前用户 查看当前用户信息,包含其拥有的权限 在这里插入图片描述

演示完毕,开始讲解 2 授权模式选择

本项目选择密码模式,原因如下, 同一个企业内部的不同产品要使用本企业的 oAuth2.0 体系。在有些情况下,产品希望能够定制化授权页面。由于是同个企业,不需要向用户展示“xxx将获取以下权限”等字样并询问用户的授权意向,而只需进行用户的身份认证即可。这个时候,由具体的产品团队开发定制化的授权界面,接收用户输入账号密码,并直接传递给鉴权服务器进行授权即可。 在这里插入图片描述

3 授权服务选择

方案1:认证服务器进行授权管理 方案2:重新定义授权管理器,在资源服务器完成授权

本项目选择的是方案1,方案2也较简单,不过要维护RBAC表。

在这里插入图片描述

4 具体操作步骤 4.1 在认证服务数据库中添加客户端信息

配置OAuth2认证允许接入的客户端的信息,因为接入OAuth2认证服务器首先人家得认可你这个客户端。 在表 oauth_client_details 中增加一条客户端配置记录,需要设置的字段如下:

client_id:客户端标识client_secret:客户端安全码,此处不能是明文,需要加密scope:客户端授权范围authorized_grant_types:客户端授权类型web_server_redirect_uri:服务器回调地址

在这里插入图片描述

4.2 在认证服务数据库中添加用户及权限信息

认证服务器可以初始化一部分,同时,也可以通过接口调用的方式添加。 在这里插入图片描述

4.3 添加依赖(根据自己的spring版本选择对应的版本) org.springframework.cloud spring-cloud-starter-oauth2 org.springframework.security spring-security-oauth2-resource-serverauth-server}/oauth/token user-authorization-uri: ${auth-server}/oauth/authorize resource: token-info-uri: ${auth-server}/oauth/check_token user-info-uri: ${auth-server}/me sso: login-path: /login

附:默认的端点 URL

/oauth/authorize:授权端点/oauth/token:令牌端点/oauth/confirm_access:用户确认授权提交端点/oauth/error:授权服务错误信息端点/oauth/check_token:用于资源服务访问的令牌解析端点/oauth/token_key:提供公有密匙的端点,如果你使用JWT 令牌的话 4.5 配置资源服务器

创建一个类继承 ResourceServerConfigurerAdapter 并添加相关注解: @Configuration @EnableResourceServer:资源服务器 @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true):全局方法拦截

@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Resource private WhiteListConfig whiteListConfig; @Override public void configure(HttpSecurity http) throws Exception { http.oauth2ResourceServer().jwt() .jwtAuthenticationConverter(jwtAuthenticationConverter()); http.exceptionHandling() .accessDeniedHandler(accessDeniedHandler()) .authenticationEntryPoint(authenticationEntryPoint()) .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(ArrayUtil.toArray(whiteListConfig.getUrls(), String.class)).permitAll() .anyRequest().authenticated(); } /** * 未授权 * * @return */ @Bean AccessDeniedHandler accessDeniedHandler() { return (request, response, e) -> { WebUtils.writeFailedToResponse(response, ResultCode.ACCESS_UNAUTHORIZED); }; } /** * token无效或者已过期自定义响应 */ @Bean AuthenticationEntryPoint authenticationEntryPoint() { return (request, response, e) -> { WebUtils.writeFailedToResponse(response, ResultCode.TOKEN_INVALID_OR_EXPIRED); }; } /*** * 定义JJwtAccessTokenConverter * @return */ @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstants.AUTHORITY_PREFIX); jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstants.JWT_AUTHORITIES_KEY); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); return jwtAuthenticationConverter; } }

4.5.1 自定义异常

当发生异常的时候,可返回自定义的消息体。

/** * 未授权 * * @return */ @Bean AccessDeniedHandler accessDeniedHandler() { return (request, response, e) -> { WebUtils.writeFailedToResponse(response, ResultCode.ACCESS_UNAUTHORIZED); }; } /** * token无效或者已过期自定义响应 */ @Bean AuthenticationEntryPoint authenticationEntryPoint() { return (request, response, e) -> { WebUtils.writeFailedToResponse(response, ResultCode.TOKEN_INVALID_OR_EXPIRED); }; }

4.5.2 白名单 对于登录和登出的接口,直接放行。 WhiteListConfig:

/** * 白名单配置 */ @Data @Configuration @ConfigurationProperties(prefix = "whitelist") public class WhiteListConfig { private List urls; }

application.yml:

whitelist: urls: - "/api/login" - "/api/logout"

4.5.3 token转换 把jwt的Claim中的authorities加入,这样就可以重新定义权限管理器了。

/*** * 定义JJwtAccessTokenConverter * @return */ @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstants.AUTHORITY_PREFIX); jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstants.JWT_AUTHORITIES_KEY); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); return jwtAuthenticationConverter; } 4.6 登录及注销

登录就是携带用户名,密码访问${auth-server}/oauth/token,获取token。

/** * 登录 * * @return */ @PostMapping("/login") public String login(@RequestBody PlatUserLoginParam loginParam) throws Exception { Map params = new HashMap(); Map header = new HashMap(); String clientId = oAuth2Properties.getClientId(); String clientSecret = oAuth2Properties.getClientSecret(); byte[] bytes = (clientId + ":" + clientSecret).getBytes("utf-8"); String encode = new BASE64Encoder().encode(bytes); header.put(AuthConstants.JWT_TOKEN_HEADER, "Basic " + encode); String response = HttpClientUtils.httpPostRequest(oAuth2Properties.getAccessTokenUri() + "?grant_type=" + loginParam.getGrantType() + "&username=" + loginParam.getUserName() + "&password=" + loginParam.getPassword(), header, params, "utf-8"); JSONObject jsonObject = JSONUtil.parseObj(response); String accessToken = null; if (jsonObject != null) { accessToken = jsonObject.getStr("access_token"); } return accessToken; }

注销 以下就JWT在某些场景需要失效的简单方案整理如下:

白名单方式 认证通过时,把JWT缓存到Redis,注销时,从缓存移除JWT。请求资源添加判断JWT在缓存中是否存在,不存在拒绝访问。这种方式和cookie/session机制中的会话失效删除session基本一致。黑名单方式 注销登录时,缓存JWT至Redis,且缓存有效时间设置为JWT的有效期,请求资源时判断是否存在缓存的黑名单中,存在则拒绝访问。 白名单和黑名单的实现逻辑差不多,黑名单不需每次登录都将JWT缓存,仅仅在某些特殊场景下需要缓存JWT,给服务器带来的压力要远远小于白名单的方式。 本项目选择黑名单方式实现 /** * 注销 * * @return */ @DeleteMapping("/logout") public Result logout() { JSONObject jsonObject = WebUtils.getJwtPayload(); String jti = jsonObject.getStr("jti"); // JWT唯一标识 long exp = jsonObject.getLong("exp"); // JWT过期时间戳 long currentTimeSeconds = System.currentTimeMillis() / 1000; if (exp @Autowired private RedisTemplate redisTemplate; /** * 拦截器执行顺序 * * @return */ @Override public int filterOrder() { return FORM_BODY_WRAPPER_FILTER_ORDER - 1; } /** * 拦截器类型 * * @return */ @Override public String filterType() { return FilterConstants.PRE_TYPE; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); HttpServletResponse response = ctx.getResponse(); // 无token放行 String token = request.getHeader(AuthConstants.JWT_TOKEN_HEADER); if (StrUtil.isBlank(token)) { return null; } // 解析JWT获取jti,以jti为key判断redis的黑名单列表是否存在,存在拦截响应token失效 token = token.replace(AuthConstants.JWT_TOKEN_PREFIX, Strings.EMPTY); JWSObject jwsObject = null; try { jwsObject = JWSObject.parse(token); } catch (ParseException e) { e.printStackTrace(); } String payload = jwsObject.getPayload().toString(); JSONObject jsonObject = JSONUtil.parseObj(payload); String jti = jsonObject.getStr("jti"); Boolean isBlack = redisTemplate.hasKey(AuthConstants.TOKEN_BLACKLIST_PREFIX + jti); if (isBlack) { ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); ctx.setResponseBody( JSON.toJSONString(CloudwalkResult.fail(GatewayCommonRespCodeEnum.RESPONSE_SERVICE_NOT_EXIST.getCode(), GatewayCommonRespCodeEnum.RESPONSE_SERVICE_NOT_EXIST.getMessage()))); ctx.getResponse().setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); return null; } // 存在token且不是黑名单,request写入JWT的载体信息 ctx.addZuulRequestHeader(AuthConstants.JWT_PAYLOAD_KEY, payload); return null; } } 4.8 获取当前用户信息 /** * 获取当前用户信息 * * @return */ @PostMapping("/me") public Result getCurrentUser() throws Exception { Map params = new HashMap(); Map header = new HashMap(); HttpServletRequest request = getHttpServletRequest(); String token = request.getHeader(AuthConstants.JWT_TOKEN_HEADER); header.put(AuthConstants.JWT_TOKEN_HEADER, token); String response = HttpClientUtils.httpGetRequest(resourceServerProperties.getUserInfoUri(), header, params, "utf-8"); JSONObject jsonObject = JSONUtil.parseObj(response); return Result.success(jsonObject); } 4.9 令牌刷新

理论背景: 在微服务项目 OAuth2实现微服务的统一认证的背景下,前端调用/oauth/token接口认证,在认证成功会返回两个令牌access_token和refresh_token,出于安全考虑access_token时效相较refresh_token短很多(access_token默认12小时,refresh_token默认30天)。当access_token过期或者将要过期时,需要拿refresh_token去刷新获取新的access_token返回给客户端,但是为了客户良好的体验需要做到无感知刷新。 方案一:浏览器起一个定时轮询任务,每次在access_token过期之前刷新。 方案二:请求时返回access_token过期的异常时,浏览器发出一次使用refresh_token换取access_token的请求,获取到新的access_token之后,重试因access_token过期而失败的请求。 方案比较: 第一种方案实现简单,但在access_token过期之前刷新,那些旧access_token依然能够有效访问,如果使用黑名单的方式限制这些就的access_token无疑是在浪费资源。 第二种方案是在access_token已经失效的情况下才去刷新便不会有上面的问题,但是它会多出来一次请求,而且实现起来考虑的问题相较下比较多,例如在token刷新阶段后面来的请求如何处理,等获取到新的access_token之后怎么重新重试这些请求。 总结:第一种方案实现简单;第二种方案更为严谨,过期续期不会造成已被刷掉的access_token还有效;总之两者都是可行方案,本项目采用第二种方案。 后端 后端部分这里唯一工作是在网关youlai-gateway鉴定access_token过期时抛出一个自定义异常提供给前端判定,如下图所示: 在这里插入图片描述 前端 1. OAuth2客户端设置 设置OAuth2客户端支持刷新模式,只有这样才能使用refresh_token刷新换取新的access_token。以及为了方便我们测试分别设置access_token和refresh_token的过期时间,因为默认的12小时和30天我们吃不消的;除此之外,还必须满足t(refresh_token) > 60s + t(access_token)的条件, refresh_token的时效大于access_token时效我们可以理解,那这个60s是怎么回事,别急还是先看实现,原因下文会说明。

{ "clientId": "youlai-mall-weapp", "clientSecret": "123456", "resourceIds": "", "scope": "all", "authorizedGrantTypes": "authorization_code,password,refresh_token,implicit", "webServerRedirectUri": null, "authorities": null, "accessTokenValidity": 3600, "refreshTokenValidity": 7200, "additionalInformation": null, "autoapprove": "true" }

2 添加刷新令牌方法 设置了支持客户端刷新模式之后,在前端添加一个refreshToken方法,调用的接口和登录认证是同一个接口/oauth/token,只是参数授权方式grant_type的值由password切换到refresh_token,即密码模式切换到刷新模式,这个方法作用是在刷新token之后将新的token写入到localStorage覆盖旧的token。 在这里插入图片描述 3 请求响应拦截添加令牌过期处理 在判断响应结果是token过期时,执行刷新令牌方法覆盖本地的token。 在刷新期间需做到两点,一是避免重复刷新,二是请求重试,为了满足以上两点添加了两个关键变量:

refreshing----刷新标识

在第一次access_token过期请求失败时,调用刷新token请求时开启此标识,标识当前正在刷新中,避免后续请求因token失效重复刷新。

waitQueue----请求等待队列

当执行刷新token期间时,需要把后来的请求先缓存到等待队列,在刷新token成功时,重新执行等待队列的请求即可。

let refreshing = false,// 正在刷新标识,避免重复刷新 waitQueue = [] // 请求等待队列 service.interceptors.response.use( response => { const {code, msg, data} = response.data if (code !== '00000') { if (code === 'A0230') { // access_token过期 使用refresh_token刷新换取access_token const config = response.config if (refreshing == false) { refreshing = true const refreshToken = getRefreshToken() return store.dispatch('user/refreshToken', refreshToken).then((token) => { config.headers['Authorization'] = 'Bearer ' + token config.baseURL = '' // 请求重试时,url已包含baseURL waitQueue.forEach(callback => callback(token)) // 已刷新token,所有队列中的请求重试 waitQueue = [] return service(config) }).catch(() => { // refresh_token也过期,直接跳转登录页面重新登录 MessageBox.confirm('当前页面已失效,请重新登录', '确认退出', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { store.dispatch('user/resetToken').then(() => { location.reload() }) }) }).finally(() => { refreshing = false }) } else { // 正在刷新token,返回未执行resolve的Promise,刷新token执行回调 return new Promise((resolve => { waitQueue.push((token) => { config.headers['Authorization'] = 'Bearer ' + token config.baseURL = '' // 请求重试时,url已包含baseURL resolve(service(config)) }) })) } } else { Message({ message: msg || '系统出错', type: 'error', duration: 5 * 1000 }) } } return {code, msg, data} }, error => { return Promise.reject(error) } ) 以上,第三方认证平台接入完成。


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3